package de.zainodis.commons.utils;

import static junit.framework.Assert.assertNotNull;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;

import android.R.integer;
import android.util.Base64;
import de.zainodis.commons.LogCat;
import de.zainodis.commons.model.CurrencyAmount;

public class TypeConverter {

   private static final String TAG = "TypeConverter";

   /**
    * Converts the given value to the requested type. Currently supported types
    * are: String, Boolean, Integer, Float, Short, Double, Long, Byte,
    * {@link Date}, Calendar, {@link Enum}, byte arrays (primitive and wrapper).
    * The provided dateTime string is expected in the format ISO8601. The Enum
    * conversion to a specific Enum type only works, if the provided value
    * equals the actual name of the enum. Enum values (such as those passed via
    * an Enum constructor) are not considered. The byte arrays are using
    * {@link Base64} for decoding the given string.
    * 
    * @param value
    *           the value that should be converted
    * @param targetType
    *           the type to which the value should be converted
    * @return an Object of the requested type, containing the given value
    * @throws IllegalArgumentException
    *            if the requested type matches none of the supported types
    */
   public static Object convertTo(String value, Class<?> targetType) {

	 assertNotNull("Target type must not be null.", targetType);

	 if (targetType.equals(String.class)) {
	    // That's the easiest case, we're done
	    return value;
	 }

	 if (value.length() == 0) {
	    // Return null if empty value is passed (non-String types only)
	    return null;
	 }

	 /*
	  * Quite annoying (double checking), but Boolean.class != boolean.class,
	  * so we need to double check all primitives.
	  */
	 if (isBoolean(targetType)) {
	    return Boolean.parseBoolean(value);

	 } else if (isInteger(targetType)) {
	    return Integer.parseInt(value);

	 } else if (isFloat(targetType)) {
	    return Float.parseFloat(value);

	 } else if (isDouble(targetType)) {
	    return Double.parseDouble(value);

	 } else if (isShort(targetType)) {
	    return Short.parseShort(value);

	 } else if (isLong(targetType)) {
	    return Long.parseLong(value);

	 } else if (isByte(targetType)) {
	    return Byte.parseByte(value);

	 } else if (targetType.equals(Date.class)) {
	    try {
		  // Attempt to parse date-time
		  return DateTimeUtils.parse(value, DateTimeUtils.DATETIME_FORMAT_ISO8601).getTime();
	    } catch (ParseException e) {
		  String errorMessage = "Failed to convert " + value
			   + " to an instance of the type java.util.Date.";
		  LogCat.e(TAG, errorMessage);
	    }

	 } else if (targetType.equals(Calendar.class)) {
	    try {
		  return DateTimeUtils.parse(value, DateTimeUtils.DATETIME_FORMAT_ISO8601);
	    } catch (ParseException e) {
		  String errorMessage = "Failed to convert " + value
			   + " to an instance of the type Calendar.";
		  LogCat.e(TAG, errorMessage);
	    }

	 } else if (targetType.equals(CurrencyAmount.class)) {
	    return new CurrencyAmount(value);
	 } else if (Enum.class.isAssignableFrom(targetType)) {
	    // Only works if the value equals the enum name of the given type
	    Class<Enum> enumType = (Class<Enum>) targetType;
	    return Enum.valueOf(enumType, value);

	 } else if (targetType.equals(byte[].class) || targetType.equals(Byte[].class)) {
	    return DeEncodingUtils.base64StringToByteArray(value);
	 }
	 // Target type is not supported, throw an error
	 throw new IllegalArgumentException("Failed to parse " + value
		  + " - none of the supported types matches " + targetType.getCanonicalName());
   }

   /**
    * Converts a given object to String and returns the result. All types are
    * supported, but unless there is specific handling of the type implemented,
    * only the default method toString of the given type is executed and the
    * result returned. Specific handling is currently implemented for the
    * following types: Date (is converted to ISO8601 String), Calendar (is
    * converted to ISO8601 String) and byte arrays (primitives and wrapper class
    * will be converted to a {@link Base64} encoded string). Booleans are
    * converted to "True" or "False".
    * 
    */
   public static String convertToString(Object value) {
	 if (value == null) {
	    return StringUtils.EMPTY;
	 }

	 if (isBoolean(value.getClass())) {
	    Boolean bool = Boolean.parseBoolean(value.toString());
	    /*
	     * In order to be compatible with the windows mobile framework, we need
	     * to return capitalized True and False instead of lower case (as done
	     * in java by default).
	     */
	    return bool ? "True" : "False";
	 } else if (value instanceof Date) {
	    return DateTimeUtils.format(DateTimeUtils.toCalendar((Date) value),
			DateTimeUtils.DATETIME_FORMAT_ISO8601);
	 } else if (value instanceof Calendar) {
	    return DateTimeUtils.format((Calendar) value, DateTimeUtils.DATETIME_FORMAT_ISO8601);
	 } else if (value instanceof Enum) {
	    // toString implementation is not reliable enough in case of change
	    return ((Enum<?>) value).name();
	 } else if (value instanceof byte[]) {
	    return DeEncodingUtils.byteArrayToBase64String((byte[]) value);
	 } else if (value instanceof Byte[]) {
	    return DeEncodingUtils
			.byteArrayToBase64String(byteWrapperArrayToPrimitiveByteArray((Byte[]) value));
	 }

	 // The default return value
	 return value.toString();
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is a {@link Boolean}; false otherwise.
    */
   public static boolean isBoolean(Class<?> type) {
	 return Boolean.class.equals(type) || boolean.class.equals(type);
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is an {@link integer}; false otherwise.
    */
   public static boolean isInteger(Class<?> type) {
	 return Integer.class.equals(type) || int.class.equals(type);
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is an {@link Float}; false otherwise.
    */
   public static boolean isFloat(Class<?> type) {
	 return Float.class.equals(type) || float.class.equals(type);
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is an {@link Long}; false otherwise.
    */
   public static boolean isLong(Class<?> type) {
	 return Long.class.equals(type) || long.class.equals(type);
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is an {@link Double}; false otherwise.
    */
   public static boolean isDouble(Class<?> type) {
	 return Double.class.equals(type) || double.class.equals(type);
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is an {@link Short}; false otherwise.
    */
   public static boolean isShort(Class<?> type) {
	 return Short.class.equals(type) || short.class.equals(type);
   }

   /**
    * 
    * @param type
    *           the type to check.
    * @return if the given type is an {@link Byte}; false otherwise.
    */
   public static boolean isByte(Class<?> type) {
	 return Byte.class.equals(type) || byte.class.equals(type);
   }

   /**
    * Converts a wrapper-class byte array to its primitive equivalent.
    */
   private static byte[] byteWrapperArrayToPrimitiveByteArray(Byte[] array) {
	 byte result[] = new byte[array.length];
	 for (int i = 0; i < array.length; i++) {
	    result[i] = array[i];
	 }
	 return result;
   }
}
